<?php
/** ================================
 *  @package IntBLite
 *  @author 4X_Pro <admin@openproj.ru>
 *  @version 3.0
 *  @copyright 2007,2009,2010 4X_Pro, INTBLITE.RU
 *  http://intblite.ru
 *  Основной модуль сайтового движка Intellect Board 3
 *  ================================ */

class Application {
  var $mode; /** режим вывода, может принимать значения 0 -- обычный, 1 -- сообщение с редиректом, 2 -- AJAX-режим **/
  var $lastmod; /** время последней модификации страницы, выдаваемое скриптом в заголовок Last-Modified **/
  var $status=200; /** HTTP-статус, выдаваемый скриптом **/
  var $action; /** запрошенное пользователем действие, определяется в функции parse_url **/
  var $location; /** Адрес для перехода при 301 или 302 статусе **/
  var $url; /** запрошенный пользователем относительный URL **/
  var $objdata; /** данные о запрошенной странице **/
  var $objparents; /** данные о родительских страницах **/
  var $main_object; /** объект для обработки выводимой страницы **/
  /** @var Database **/
  var $db;
  /** @var auth **/
  var $user; /** класс модуля auth для аутентификации текущего пользователя **/
  var $sitepath; /** путь к корню движка**/
  var $messages=array(); /** информационные сообщения, ошибки и предупреждения **/
  var $mail=array(); /** сообщения для отправки Email **/
  var $mail_return; /** Сюда запоминаем адрес возврата ошибочных Email **/
  var $meta; /** Мета-теги, дополнительные CSS-файлы и JS-файлы и т.п. **/
  var $content_type; /** Заголовок Content-type **/
  var $headers=array(); /** Вспомогательные заголовки **/
  var $start_time=array(); /** Время начала выполнения скрипта **/
  /** Главная функция, из нее вызывается все остальное
   Действия:
   * Инициализация
   * Разбор URL и выделение из него части, которая обрабатывается скриптом (удаление части пути до корня движка)
   * Извлечение данных о запрошенном объекте
   * TODO: определение шаблона для вывода
   * TODO: выдача предварительной части страницы (заголовок и логотип) для ускорения времени ответа сайта (?)
   * Подключение модуля, который обрабатывает объекты запрошенного типа
   * Вызов процедуры, выполняющей запрошенное действие
   * Загрузка основного файла шаблона default.htm
   * TODO: обработка дополнительных блоков файла шаблона
   * Вывод заголовков и данных
   * Завершающие процедуры (например, закрытие соединения с БД)
   **/
  function main() {
    $this->init();

    $this->checkpoint('Конец инициализации');

    $url=$this->parse_url();

    // получение информации о запрошенной странице
    $parent_urls=$this->parent_items($this->url); //
    $this->objparents = Module::get_objects($this->db->array_to_sql($parent_urls, 'url'),'url');
    if (!empty($this->objparents) && $this->objparents[count($this->objparents)-1]['url']==$this->url) $this->objdata=&$this->objparents[count($this->objparents)-1]; // если удалось извлечь данные по объектам, то последний объект и будет тем, для которого сделан запрос
    else $this->objdata=false;

    $template='default'; // шаблон по умолчанию называется default, дальше он может быть переопределен
    // TODO: здесь сделать вывод начальной части сайта из кеша для конкретного пользователя
    // и подумать, в каких случаях это применять

    $this->checkpoint('Объекты получены');

    // выполнение запрошенного действия, если статус равен 200 (т.е. пользователь не забанен)
    if ($this->status==200 && $this->objdata && $this->load_module($this->objdata['module'])) { // если URL найден в базе и удалось подключить модуль для его обработки
      $this->lastmod = $this->objdata['lastmod'];
      if (isset($_SESSION['starttime']) && !$this->user->is_guest()) $this->lastmod = max($this->objdata['lastmod'],$_SESSION['starttime']); // время модификации равно времени того, что случилось позже: последняя модификация данной страницы или начало сессии пользователя
      $classname= $this->objdata['module'];
      $this->main_object = new $classname($this->objdata);
      if (!$this->action) $this->action='view';
      $main_data=$this->main_object->process_action($this->action);
      $template=$this->objdata['template'];
    }
    elseif ($this->status==200) { // если объект не найден в базе или не удалось загрузить модуль для него
      $this->status=404;
      $this->message('Страница с заданным URL не найдена!',3);
      $this->mode=1;
      $main_data=false;
    }
    $this->checkpoint('Основное действие выполнено');

    $output_template=$this->output_prepare($main_data); // подготовка к выводу -- вызов процедур генерации вспомогательных блоков и подготовка основного шаблона
    $hurl = $this->hurl(''); // HURL корня сайта

    if (session_id()!=false) session_write_close(); // сессию закрываем здесь, так как дальше закрывается соединение с БД и нужно успеть ее сохранить
    $this->db->close(); // закрываем соединение с БД как можно раньше -- до парсинга и

    if ($output_template) { // если предполагается выводить что-то кроме заголовков
      $parser = new Parser();
      $output_data=$parser->parse($template,$output_template);
      $output_data = str_replace('##SITEPATH#/',$this->sitepath,$output_data);
      $output_data = str_replace('##SITEHURL#/',$hurl.'/',$output_data);
    }
    else $output_data=false;
    $this->output($output_data); // выводим данные и закрываем наконец-то буфер

    $this->checkpoint('Вывод выполнен',true);

    $this->process_mail(); // отправляем почту в случае необходимости
    $this->checkpoint('Почта отправлена',true);
  }

  /** Функция общей инициализации: подключение обработчиков ошибок, буферизации и т.п. **/
  function init() {
    $this->start_time=array_sum(explode(' ',microtime())); // фиксируется время запуска скрипта;
    error_reporting(E_ALL);
    $GLOBALS['IntB_debug']='';
    setlocale(LC_ALL,'ru_RU.UTF-8','ru_RU.utf-8');

    set_error_handler(array($this,'error_handler')); // вешаем собственный обработчик ошибок для фиксации их в логах и выдачи дружественных сообщений
    register_shutdown_function(array($this,'shutdown')); //
    set_magic_quotes_runtime(0);
    if (get_magic_quotes_gpc()) $this->strip_arrays();
    if (version_compare(phpversion(),'5.1.0','>=')) date_default_timezone_set('Europe/Moscow');

    // определяем sitepath и парсим URL
    $file=__FILE__;
    $this->sitepath=preg_replace('|/+|','/',str_replace(basename($file),'',$_SERVER['SCRIPT_NAME'])); //TODO: может быть, сделать проще: dirname($_SERVER['sciprt_name']) ?

    $this->init_db(); // инициализация подключения к БД
    $this->init_config(); // подключение файла конфигурации

    if ($this->get_opt('gzip')) ob_start('ob_gzhandler',9);
    else ob_start(); // если выключена поддержка GZIP, используем простую буферизацию
    $this->meta=array();

    $this->init_user(); // инициализация пользовательской библиотеки
  }

  /** Инициализация базы данных **/
  function init_db() {
    if (!include('config/database.php')) {
      header($_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable');
      if (is_dir('install')) {
        echo '<html><head><title>Intellect Board Lite еще не установлен!</title></head><body><h2>Intellect Board Lite еще не установлен!</h2><a href="install/">Провести установку</a></body></html>';
        exit();
      }
      echo 'Ошибка: файл config/database.php поврежден, отсутствует или недоступен для чтения!';
      exit();
    }

    require 'database/database.php'; // общий модуль базы данных

    if (!isset($GLOBALS['database']['driver']) || !include_once('database/'.$GLOBALS['database']['driver'].'.php')) {
      header($_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable');
      echo 'Ошибка: Некорректно указан тип используемой БД или модуль работы с БД поврежден!';
      exit();
    }

    define('DB_prefix',$GLOBALS['database']['prefix']);
    $classname='Database_'.$GLOBALS['database']['driver'];
    $this->db = new $classname;
    $this->db->connect($GLOBALS['database']);
  }

  /** Подключение файла конфигурации **/
  function init_config() { // Для IntB Lite сейчас все настройки хранятся в базе
  }

  /** Подлючение библиотеки аутентификации пользователя **/
  function init_user() {
    $this->load_module('auth',true);
    $this->user = new auth;
    $activation=$this->get_opt('user_activation');
    /*    if ($activation!=0 && !$this->user->is_activated()) {
      $this->status=403;
      $this->mode=1;
      if ($activation==1) $this->message('Ваш профиль не был активирован! Вам необходимо перейти по ссылке, которая была выслана на указанный вами адрес EMail для того, чтобы его активировать! Если письмо не было доставлено, вы можете сменить адрес EMail, выбрав соответствующую ссылку при регистрации.',3);
      else $this->message('Ваш профиль не был активирован! Активация на данном форуме осуществляется администратором. Пожалуйста, наберитесь терпения!',3);
      $this->user->set_user($this->user->load_guest());
    }
    if ($this->user->is_banned()) {
      $this->status=403;
      $this->mode=1;
      $this->message('Вы изгнанны с этого форума. Если вы считаете, что произошла ошибка, обратитесь к администрации сайта.',3);
    }*/
  }

  /** Точка контроля производительности движка
   *
   * @param string $name Имя контрольной точки
   * @param bool $output Способ вывода: false -- с помощью dbg, true -- напрямую, с помощью echo.
   */
  function checkpoint($name,$output=false) {
    if ($GLOBALS['debug']['mode']>=2) {
      $tpass=$this->time_passed();
      $msg='Контрольная точка "'.$name.'". Время выполнения: '.sprintf('%.3f',$tpass);
      if (is_object($this->db)) {
        $msg.='. Запросов: '.$this->db->query_count.', время запроса: '.sprintf('%.3f (%.2f)%%',$this->db->query_time,($this->db->query_time/$tpass)*100);
      }
      if (function_exists('memory_get_usage')) {
        $msg.='. Памяти использовано: '.memory_get_usage().' байтов';
      }
      if (!$output) _dbg($msg);
      else echo $msg.'<br />';
    }
  }

  /** Высчитывает, сколько времени прошло с указанного момента. Если момент не указан явно, за него берется начало времени выполнения скрипта
   *  @return float  **/
  function time_passed() {
    return array_sum(explode(' ',microtime()))-$this->start_time;
  }

  /** Сохранение сообщения для последующего вывода **/
  function message($message,$level=1) {
    if (is_string($message)) $this->messages[]=array('text'=>$message,'level'=>$level);
    elseif (is_array($message) && isset($message['text'])) $this->messages[]=$message;
    else $this->messages=array_merge($this->messages,$message);
  }

  /** Выборка из хеш-массива $data только тех элементов, ключи которых есть в массиве $keys.
   * Используется для фильтрации данных, переданных из формы или перед сохранением в БД.
   * Обработка провощится нерекурсивно.
   *
   * @param array $data Исходные данные
   * @param array $keys Список допустимых ключей для массива исходных данных
   * @return array Отфильтрованный масссив
   */
  function filter($data,$keys) {
    $result=array();
    if (is_array($keys)) {
      for ($i=0, $count=count($keys); $i<$count; $i++) if (isset($data[$keys[$i]])) $result[$keys[$i]]=$data[$keys[$i]];
    }
    return $result;
  }

  /** Получение всех настроек данной сущности в виде хеш-массива.
   *
   * @param string $type Тип сущности. В данный момент может быть 'global', 'user' и 'object'
   * @param int $id Идентификатор сущности (для сущности global должен быть равен нулю).
   */
  function get_opt_all($type='global',$id=0) {
    $sql = 'SELECT name, value FROM '.DB_prefix.'option WHERE oid='.intval($id).' AND type="'.$this->slashes($type).'"';
    $settings=$this->db->select_simple_hash($sql);
    return $settings;
  }

  /** Получение значения переменной из настроек **/
  function get_opt($name,$type='global',$id=0) {
    static $opt_cache;
    if ($type=='user' && ($id==0 || $id==$this->user->get_uid())) { // если запрос настроек делается для текущего пользователя и они хранятся в сессии, то берем их оттуда
      if (isset($_SESSION['cache']) && isset($_SESSION['cache']['settings']) && isset($_SESSION['cache']['settings'][$name])) $result=$_SESSION['cache']['settings'][$name];
      else {
        $_SESSION['cache']['settings']=$this->get_opt_all($type,$this->user->get_uid());
        $result=(isset($_SESSION['cache']['settings'][$name])) ? $_SESSION['cache']['settings'][$name] : false;
      }
    }
    else { // если заправшиваются настройки какой-либо другой сущности, проверяем, есть ли они в кеше, и если нет, получаем их туда
      if (!isset($opt_cache[$type.$id])) $opt_cache[$type.$id]=$this->get_opt_all($type,$id);
      $result=(isset($opt_cache[$type.$id][$name])) ? $opt_cache[$type.$id][$name] : false;
    }
    return $result;
  }

  /** Формирование человекочитаемого URL форума с учетом пути и настройки использования mod_rewrite. **/
  function hurl($url) {
    if ($this->get_opt('mod_rewrite')) { // если в настройках включено использование mod_rewrite, то путь формируется просто: путь_к_форуму/HURL
      $result=$this->sitepath.'/'.$url;
    }
    else {
      $result=$this->sitepath.basename(__FILE__).$url; // иначе путь имеет вид путь_к_форуму/index.php/HURL
    }
    return $result;
  }

  /** Получение IP-адреса в числовом формате (нужно для логов, сообщений и т.п.).
   *
   * @return int
   */
  function get_ip() {
    return ip2long($_SERVER['REMOTE_ADDR']);
  }

  /** Проверка, имеется ли значение в списке (проверка идет по value1).
   *
   * @param string $listname Название списка
   * @param string $value Значение
   * @return bool TRUE, если имеется
   */
  function check_list($listname,$value) {
    $sql = 'SELECT COUNT(*)  FROM '.DB_prefix.'list_value WHERE list="'.$this->slashes($listname).'" AND value1="'.$this->slashes($value).'"'; // TODO: возможно, доделать проверку по LIKE
    $result = $this->db->select_int($sql);
    return $result;
  }

  /** Получение значений из списка задаваемых настроек в виде массива **/
  function get_settings($listname) {
    $sql = 'SELECT id,itemname,descr,value,tag  FROM '.DB_prefix.'settings WHERE listname="'.$this->slashes($listname).'" ORDER BY sortfield';
    $result = $this->db->select_all_rows($sql);
    return $result;
  }

  /** Получение значений из простого списка в виде массива **/
  function get_list($listname) {
    $sql = 'SELECT value1,value2  FROM '.DB_prefix.'list_value WHERE list="'.$this->slashes($listname).'"';
    $result = $this->db->select_all_rows($sql);
    return $result;
  }

  /** Загрузка модуля с указанным именем **/
  function load_module($name,$fatal=false) {
    $filename = 'modules/'.$name.'.php';
    $result=false;
    if ($this->valid_file($filename,true,true)) {
      if (include_once($filename)) $result=true;
      else trigger_error('При подгрузке класса '.$name.' произошла ошибка!',($fatal) ? E_USER_ERROR : E_USER_WARNING);
      if (!class_exists($name)) trigger_error('В файле '.$filename.' класс '.$name.' не найден!',($fatal) ? E_USER_ERROR : E_USER_WARNING);
      return true;
    }
    elseif ($fatal) trigger_error('Не удается найти класса '.$name.' или его имя содержит некорректные символы',E_USER_ERROR);
    return $result;
  }

  /** Подготовка к выводу основных данных **/
  function output_prepare($content) {
    if ($this->status==200 && ($this->mode==0 || $this->mode==4)) { // если страница выводится в нормальном режиме, запускаем модули для вывода вспомогательных блоков
      if ($this->mode==4) $result = new Html('admin');
      else $result = new Html('index');
      $sql = 'SELECT name, action, module, params FROM '.DB_prefix.'template_block WHERE template_id='.intval($this->objdata['tp_id']).' ORDER BY sortfield';
      $blocks = $this->db->select_all($sql);
      for ($i=0, $count=count($blocks); $i<$count; $i++) {
        $module_name=$blocks[$i]['module'];
        if ($this->load_module($module_name,false)) {
          $curmodule = new $module_name;
          $action=$blocks[$i]['action'];
          if (method_exists($curmodule,$action)) $result->subtemplate($blocks[$i]['name'],call_user_func(array($curmodule,$action),$blocks[$i]['params']));
          else _dbg('Процедура '.$action.' в модуле '.$module_name.' не найдена, действие не выполнено!');
        }
        else _dbg('Модуль '.$blocks[$i]['module'].' не найден, действие '.$blocks[$i]['action'].' не выполнено!');
      }
      $result->subtemplate('CONTENT',$content);
      $result->set('META',$this->meta);
    }
    elseif ($this->status==404 || $this->status==403) {
      $result = new Html('error');
    }
    elseif (($status==301 || $status==302) && $mode==0) {
      if ($this->user->session()) { // если удается сохранить сообщение в сесиию, то не выводим его, а сразу делаем редирект (и сообщение будет выведено на следующей странице
        $result=false;
        if (count($this->messages)>0) $_SESSION['messages']=$this->messages; // буферизуем сообщения в сессию, чтобы вывести их на следующей нормальной странице и избавиться тем самым от вывода промежуточной страницы с редиректом
      }
      else {
        $result = new Html('message');
      }
    }
    elseif ($this->mode==1) {
      $result = new Html('message');
    }
    elseif ($this->mode==2) $result=$content;

    $this->checkpoint('Вспомогательные блоки получены');

//    $pagedata['OPTIONS']=$this->get_opt_all(); // получаем все глобальные настройки для вывода // TODO: подумать, может быть, все не надо, а достаточно только названия и описания форума
    if ($result) { // если на выходе не пустое сообщение
      if (is_object($this->main_object)) $result->set('TITLE',$this->main_object->data['title']);
      if ($this->location) $result->set('LOCATION',$this->location);
      if (isset($GLOBALS['IntBF_debug'])) $result->set('DEBUG',$GLOBALS['IntBF_debug']); // выводим отладочные сообщения
      if (isset($_SESSION['messages']) && is_array($_SESSION['messages'])) { // если с предыдущего действия остались буферизованные в сессию сообщения
        $this->messages=array_merge($this->messages,$_SESSION['messages']);
        unset($_SESSION['messages']); // и сбрасываем буфер, чтобы эти сообщения не выводились в дальнейшем
      }
      $result->set('MESSAGES',$this->messages); // добавляем буфер сообщений в результат вывода основной части
    }

    return $result;
  }

  /** Вывод итогов выполнения скрипта */
  function output($data) {
    if ($data) {
      $hash=md5($data);
      $len=strlen($data);
    }
    else {
      $hash='';
      $len=0;
    }
    if ($this->output_headers($len,$hash)) {
      print $data;
      ob_end_flush();
    }
  }

  /** Прием сообщения для отправки по Email
   * (сообщение кладется в буфер, реальная отправка происходит только перед самым завершением работы скрипта)
   * Поля структуры maildata:
   *   to --адрес получателя
   *   from -- адрес отправителя
   *   from_name -- имя отправителя
   *   to_name -- имя получателя
   *   subj -- тема письма
   *   text -- текст письма
   *   reply -- адрес отправки ответа
   * Используемые настройки (функция get_opt):
   *   email_enabled -- почтовые функции включены
   *   email_from -- адрес отправки по умолчанию
   *   site_title -- название отправителя соответствует названию сайта
   *   email_return -- адрес возврата писем с ошибками
   */
  function mail($maildata) {
    if ($this->get_opt('email_enabled')) {
      if (!$maildata['from']) {
        $maildata['from']=$this->get_opt('email_from');
        $maildata['from_name']=$this->get_opt('site_title');
      }
      $this->mail_return=$this->get_opt('email_return');
      $this->mail[]=$maildata;
    }
  }

  /** В случае, если требуется отправка почты (почтовый буфер не пуст), подключает модуль mail и передает ему буфер с почтовыми сообщениями для отправки **/
  function process_mail() {
    if (count($this->mail)>0) {
      if (!$this->load_module('mail')) trigger_error('Не удалось загрузить модуль отправки почты! Рассылка не будет произведена', E_USER_WARNING);
      else {
        $mailsender = new mail();
        $mailsender->process_mail($this->mail,$this->mail_return);
      }
    }
  }

  /** Делает запись в лог с сохранением всех параметров, которые в дальнейшем могут потребоваться для отладки
   *
   * @param string $log -- название лога
   * @param int $code -- код записи
   * @param string $file -- имя файла, в котором произошло логгируемое событие
   * @param string $text -- текст сообщения
   */
  function log_entry($log,$code,$file,$text,$line='') {
    $buffer=date('Y-m-d H:i').','.$code.','.str_replace(',','\\,',$_SERVER['REQUEST_URI']).','.
            str_replace(',','\\,',isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '').','.$file.','.$line.','.
            $_SERVER['REMOTE_ADDR'].','.str_replace(',','\\,',isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : '').
            ','.str_replace(',','\\,',$text)."\n";
    $filename='logs/'.$log.'.csv';
    echo $text;
    if ($this->valid_file($filename)) {
      $fh=fopen($filename,'a+');
      if ($fh) {
        fputs($fh,$buffer);
        fclose($fh);
      }
      else _dbg('Не удалось сохранить событие в лог '.$log.'.csv!');
    }
    else _dbg('Некорректное имя лог-файла '.$log.'.csv!');
  }


  /** Выдача HTTP-заголовков
   **/
  function output_headers($length=0,$hash='') {
    if (headers_sent()) return true;
    // настройка умолчаний, если они не были произведены ранее по каким-то причинам
    if (!isset($this->status)) $this->status=200; // по умолчанию статус равен 200
    if (!isset($this->lastmod)) $this->lastmod=time(); // по умолчанию дата последней модификации -- сейчас
    $hash='W/"'.$hash.'"';

    // проверка, можно ли взять страницу из кеша на стороне клиента
    $cache = true;
    if (!$this->get_opt('nocache') && (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || isset($_SERVER['HTTP_IF_NONE_MATCH']))) {
      if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // проверяем If-Modified-Since
        $time = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
        if ($this->lastmod>$time) $cache=false; // если документ обновлялся после даты, указанной в кеше, не кешируем!
      }
      if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH']!='*') {
        if (strpos($_SERVER['HTTP_IF_NONE_MATCH'],',')===false) $matches=array($_SERVER['HTTP_IF_NONE_MATCH']);
        else $matches=explode(', ',$matches);
        if (!in_array($hash,$matches)) $cache=false;
      }
    } // если nocache не включен и есть либо If-Modified-Since, либо If-None-Match, выдаем из кеша
    else $cache=false;
    if ($cache && $this->status==200) { // статусы, отличающиеся от 200, не кешируем
      if ($hash==$_SERVER['HTTP_IF_NONE_MATCH']) $this->status=304;
    }

    // выдача статусов
    if ($this->status==204) {
      header($_SERVER['SERVER_PROTOCOL'].' 204 No Content');
      ob_end_clean();
      return false;
    }
    elseif ($this->status==304) {
      header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified');
      header('Last-Modified: '.date('r',$this->lastmod));
      if ($_SERVER['SERVER_PROTOCOL']=='HTTP/1.1') header('ETag:'.$hash); // для HTTP 1.1 выдаем ETag (MD5-хеш от страницы)
      ob_end_clean();
      return false;
    }
    elseif ($this->status==301) {
      if (strpos($this->location,'://')===false) {
        if (isset($_SERVER['HTTPS'])) $this->location='https://'.$_SERVER['HTTP_HOST'].$this->location;
        else $this->location='http://'.$_SERVER['HTTP_HOST'].$this->location;
      }
      header($_SERVER['SERVER_PROTOCOL'].'301 Moved Permanently');
      header('Location: '.$this->location);
      return false;
    }
    elseif ($this->status==302) {
      if (strpos($this->location,'://')===false) {
        if (isset($_SERVER['HTTPS'])) $this->location='https://'.$_SERVER['HTTP_HOST'].$this->location;
        else $this->location='http://'.$_SERVER['HTTP_HOST'].$this->location;
      }
      header($_SERVER['SERVER_PROTOCOL'].'302 Moved Temporary');
      header('Location: '.$this->location);
      return false;
    }
    elseif ($this->status==404) header($_SERVER['SERVER_PROTOCOL'].' 404 Not Found');
    elseif ($this->status==503) header($_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable');
    elseif ($this->status==410) header($_SERVER['SERVER_PROTOCOL'].' 410 Gone');
    else header($_SERVER['SERVER_PROTOCOL'].' 200 OK');

    if (!$this->content_type) $this->content_type='text/html; charset=utf-8';
    header('Content-Type: '.$this->content_type);
    for ($i=0, $count=count($this->headers); $i<$count; $i++) header($this->headers[$i]); // выводим дополнительные заголовки, если таковые есть
// 		if ($length) header('Content-length: '.$length); //TODO: доделать проверку на использование ZLib

    header('Last-Modified: '.date('r',$this->lastmod));
    if ($_SERVER['SERVER_PROTOCOL']=='HTTP/1.1') header('ETag:'.$hash); // для HTTP 1.1 выдаем ETag (MD5-хеш от страницы)
    if ($_SERVER['REQUEST_METHOD']=='HEAD') return false;
    return true;
  }

  /** Получение HURL страницы, запрошенной пользователем.
   Поддерживаются 2 режима его получения: через /index.php/HURL и через mod_rewrite  **/
  function parse_url() {
    if ($this->sitepath!='/') $url = str_replace($this->sitepath,'/',$_SERVER['PHP_SELF']);
    else $url = $_SERVER['PHP_SELF'];
    $index='/'.basename(__FILE__);
    if (substr($url,0,strlen($index))==$index) $url=substr($url,strlen($index));
    if ($url=='') $url='/';
    if (substr($url,-4,4)!='.htm' && substr($url,-1,1)!='/' && $url!='/') {
      if (isset($_SERVER['HTTPS'])) $this->location='https://';
      else $this->location='http://';
      // TODO: сделать добавление http:// и хоста в output_headers, а не здесь
      $this->location.=$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].'/';
      if ($_SERVER['QUERY_STRING']) $this->location.='?'.$_SERVER['QUERY_STRING'];
      $this->status=301;
      $this->output_headers(false,0,false);
      exit();
    }
    if (substr($url,-4,4)=='.htm') {
      $this->action=basename($url,'.htm'); // именем действия является часть URL, представляющая собой "имя файла" с расширением .htm
      if (!$this->action) $this->action='view';
      $this->url=dirname($url); // URLом объекта является часть URL, соответствующая пути к сайту
      // TODO: добавить проверку на $action==view и сделать редирект на адрес без view.htm для избежания дублирования страниц
    }
    else {
      $this->url=substr($url,0,-1); // отсекаем / в конце полученного URL, т.к. в базе URL объекта хранятся без конечного /
      $this->action='view';
    }
    if ($this->url=='/') $this->url='';
  }

  function parent_items($url) {
    if ($url=='/' || $url=='\\' || $url=='') return array('');
    $parents=$this->parent_items(dirname($url));
    //TODO: проверка на отсутствие / в конце $url
    array_push($parents, $url);
    return $parents;
  }

  /** Проверка, работаем ли мы в режиме Центра Администрирования (нужно для модулей администрирования, которые работают только оттуда).
   * Для основного приложения это утверждение всегда неверно, поэтому возвращаем false.
   * Для собственно модуля администрирования здесь будет дополнительная аутентификация.
   */
  function is_admin_mode() {
    return false;
  }

  /** Проверка, будет ли выдаваться статус 304 по результатам выполнения действия
   * Проверяется параметр настройки nocache, наличие заголовка If-Modified-Since и время последнего обновления страицы
   *
   * @return bool -- true, если будет 304
   */
  function check_last_mod() {
    if ($this->get_opt('nocache') || !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) || strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$this->lastmod) return false;
    else return true;
  }

  /** Добавление META-тега в буфер meta **/
  function meta($content,$value) {
    $tag['type']='meta';
    $tag['content']=$content;
    $tag['value']=$value;
    $this->meta[]=$tag;
  }

  /** Добавление тега LINK в буфер meta **/
  function link($href,$rel, $id=false) {
    $tag['type']='script';
    $tag['href']=$href;
    $tag['rel']=$rel;
    if ($id) $tag['id']=$id;
    $this->meta[]=$tag;
  }

  /** Добавление тега SCRIPT в буфер meta **/
  function script($src,$defer=false) {
    $tag['type']='script';
    $tag['src']=$src;
    if ($defer) $tag['defer']='defer';
    $this->meta[]=$tag;
  }

  // TODO: добавить функцию noindex

  /** Проверка, что выполняемый запрос отправлен POST-методом **/
  function is_post() {
    return ($_SERVER['REQUEST_METHOD']=='POST');
  }

  /** Определение страницы, на которую следует вернуть пользователя после выполнения следующего шага.
   * Выполняется следующим образом: если в параметрах запроса есть поле referer, адрес берется из него.
   * Если нет, берем поле HTTP_REFERER и проверяем на наличие правильного URL (символов ://).
   * Если такие символы отсутствуют (например, некоторые firewallы там пишут BLOCKED BY...), то помещаем туда главную страницу движка.
   */
  function referer() {
    if (isset($_REQUEST['referer'])) $result=$_REQUEST['referer'];
    elseif (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'],'://')!==false) {
      $result=$_SERVER['HTTP_REFERER']; // если указана явно страница возврата, возвращаемся на нее, иначе -- туда, откуда зашли на текующую страницу
    }
    else $result=$this->sitepath; //TODO: посмотреть, нужно добавлять / в конец принудительно
    return $result;
  }

  /** Функция проверяет имя файла на наличие недопустимых символов и, при необходимости, существование
   При $debug!=false информация об отсутствии файла сохраняется в отладочную информацию. **/
  function valid_file($filename,$exist=false,$debug=false) {
    $result=(substr($filename,0,1)!='/' && substr($filename,0,1)!='\\' && substr($filename,0,1)!='~');
    if ($result) {
      $test=array('..','://','`','\'','"',':',';',',','&','>','<');
      for ($i=0, $count=count($test); $i<$count && $result; $i++) $result=(strpos($filename,$test[$i])===false);
    }
    if ($result && $exist) {
      $result=is_readable($filename);
      if (!$result && $debug) _dbg('Файл '.$filename.' не найден или недоступен для чтения!');
    }
    return $result;
  }

  /** Функция проверки корректности Email (по формату) **/
  function valid_email($email) {
    return preg_match('|[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9\.\-]|',$email);
  }

  /** Экранирование кавычек в SQL-запросах. **/
  function slashes($sql) {
    return addslashes($sql); //TODO: сделать использование средств БД для экранирования
  }


  function shutdown() {
    if (is_object($this->db)) $this->db->close();
  }

  /** Удаление "magic quotes" из массивов GET, POST, REQUEST, COOKIES **/
  function strip_arrays() {
    $_GET=str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_GET);
    $_POST=str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_POST);
    $_COOKIE=str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_COOKIE);
    $_REQUEST=str_replace(array('\\"','\\\'','\\`','\\\\'),array('"','\'','`','\\'),$_REQUEST);
  }

  /** Обработчик ошибок. Выдает статус 500 и выводит дружественное сообщение об ошибке **/
  function error_handler($errno, $errstr, $errfile, $errline) {
    static $callnumber;

    $dbg_on = isset($GLOBALS['debug']['mode']) ? $GLOBALS['debug']['mode'] : false;
    $debug=debug_backtrace();
    $errstr=str_replace("\t",' ',$errstr);
    $filemsg=$errno."\t".$errstr."\t".$errline."\t";
    $errmsg='<p>'.$errstr.' (строка '.$errline.', '.$errfile.', ошибка: '.$errno.')'.'</p><ul style="font-size: 0.9em; color: #600">';
    $count=count($debug);
    for ($i=1; $i<$count; $i++) {
      $errmsg.='<li>'.$debug[$i]['function'].'()'.
              ' &mdash; '.((isset($debug[$i]['file'])) ? $debug[$i]['file'] : 'неизвестный файл').', '.
              'строка '.((isset($debug[$i]['line'])) ? $debug[$i]['line'] : 'неизвестна').'</li>'; //.var_dump($debug[$i]['args'])
    }
    $filemsg.=$debug[1]['function'].' '.
            (isset($debug[1]) && (isset($debug[1]['line'])) ? $debug[1]['line'] : 'unknown')."\t".
            (isset($debug[1]) && (isset($debug[1]['file'])) ? $debug[1]['file'] : 'unknown')."\t";
    $filemsg.='http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']."\t".date('r')."\t".$_SERVER['REMOTE_ADDR']."\t";
    $errmsg.='</ul>';

    if (($errno & E_ERROR) || ($errno & E_USER_ERROR)) {
      if (!headers_sent() && $callnumber==0) {
        $this->log_entry('error',$errno,$errfile,$errstr);
        header($_SERVER['SERVER_PROTOCOL'].' 500 Internal Server Error');
        header('Content-Type: text/html; charset=utf-8');
        echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
				"http://www.w3.org/TR/html4/loose.dtd">
				<head>
				<title>Ошибка сайта</title>
				<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
				</head><body>';
      }
      $callnumber++;

      $email = $GLOBALS['error']['email']; // EMail для отправки сообщений об ошибке храним в том же конфиг-файле, что и настройки БД, чтобы он был доступен даже в ситуации, когда база не работает
      echo '<div style="font-size: 1em; padding: 4px; font-weight: bold; color: #C44; border: #C00 1px solid; margin: 4px">
			На сайте произошла ошибка. Пожалуйста, сообщите о ней администратору сайта по адресу <a href="mailto:'.$email.'">'.$email.'</a> ';
      if ($dbg_on) {
        echo '<p>'.$errmsg.'</p>';
        echo 'Вспомогательная отладочная информация: '.$GLOBALS['IntB_debug'];
      }
      echo '</div></body></html>';
    }
    else {
      if ($dbg_on) $this->log_entry('warn',$errno,$errfile,$errstr);
      _dbg(str_replace('<p>','',str_replace('</p>','',$errmsg)));
    }
  }
}

class Module {
  var $data;

  /** Сохраняем данные объекта **/
  function  __construct($data=false,$mode=0) {
    global $app;
    $time=time();

    if ($mode==0) $this->data=$data;
    else $this->data=$app->filter($data,array('id','title','descr','url','newurl'));
    if (!isset($this->data['locked'])) $this->data['locked']=0;
    if (!isset($this->data['status'])) $this->data['status']=0;
    if (!isset($this->data['owner'])) $this->data['owner']=$app->user->get_uid();
    if (!isset($this->data['created'])) $this->data['created']=$time;
    if (!isset($this->data['lastmod'])) $this->data['lastmod']=$time;
    if (!isset($this->data['template_id'])) $this->data['template_id']=1; // TODO: подумать, как определять шаблон для новых объектов
    // в конце конструкторов дочерних объектов нужно будет задавать тип!
  }
  /** Обработка запрошенного действия **/
  function process_action($action,$params=false) {
    if (method_exists($this,'action_'.$action)) return call_user_func(array($this,'action_'.$action),$params);
    else trigger_error('Не найдено запрошенное действие!',E_USER_ERROR); // TODO: переделать на выдачу статуса 404
    return '';
  }

  /** Возвращает описание действия для списка пользователей, находящихся онлайн **/
  function get_action_description($action) {
    return false;
  }

  /** Возвращает краткое описание текущего действия для указателя положения на форуме (процедуры location в модуле menu) **/
  function get_location($action) {
    return false;
  }

  /** Возвращает URL RSS-ленты, если данный модуль способен ее генерировать, или false, если RSS-лента не предусмотрена **/
  function get_rss($action) {
    return false;
  }

  /** Извлечение данных об объектах из базы.
   *
   * @param string $condition Условие для подстановки во WHERE-часть запроса.
   * @param string $order Порядок сортировки объектов
   * @return array Массив с данными извлеченных объектов
   *
   * TODO: сделать вомзожность "малой выборки" (без типа и шаблона)
   */
  static function get_objects($condition,$order=false) {
    global $app;
    $sql = 'SELECT o.*, ot.module, tp.id AS tp_id, tp.dir AS template, url'.
            ' FROM '.DB_prefix.'object o, '.DB_prefix.'object_type ot, '.DB_prefix.'template tp, '.DB_prefix.'url url'.
            ' WHERE '.$condition.' AND o.type_id=ot.id AND o.template_id=tp.id AND url.oid=o.id ';
    if ($order) $sql.=' ORDER BY '.$order;
    $result = $app->db->select_all($sql);
    return $result;
  }

  /** Сохранение объекта в базу
   * @param mixed $parent Родительский объект (нужен для формирования URL)
   *
   * Дата создания или последней модификации фиксируются автоматически (подума
   */
  function save_object($parent,$newlastmod=false) {
    global $app;
    $time = time();
    $savedata = $app->filter($this->data,array('id','type_id','template_id','title','descr',
            'subitems','posts','owner','last_user','created','lastmod','locked','status','sort','floatsort'));

    if (!isset($this->data['id']) || !$this->data['id']) { // если у объекта нет своего id, то сохраняем его как новый объект
      if (isset($this->data['newurl']) && $this->data['newurl']) {
        $url=$this->data['newurl'];
      }
      else $url=false;
      if (!$newlastmod) $this->data['lastmod']=$time;
      else $this->data['lastmod']=$newlastmod;
      /*      if (isset($this->data['newurl'])) unset($this->data['newurl']);
      $this->data['created']=$time;
      $this->data['owner']=$app->user->get_uid();
      */
      $this->data['id']=$app->db->insert(DB_prefix.'object',$savedata);

      if (is_object($parent)) $parent=$parent->data; // если в качестве родителя передан объект, извлекаем его данные
      if (is_array($parent)) $parent=$parent['url']; // если в качестве объекта передан массив, извлекаем из него URL
      if (!$url) $url=$this->data['id'];
      $url=$parent.'/'.$url; // TODO: подумать, где сделать проверку на валиднось URL и его уникальность
      $urldata['url']=$url;
      $urldata['site']=1; // TODO: доделать мультисайтовость
      $urldata['oid']=$this->data['id'];
      $app->db->insert(DB_prefix.'url',$urldata);
    }
    else {
      $url=$this->data['url'];
      /*      $this->data['lastmod']=$time;
      unset($this->data['url']);
      if (isset($this->data['newurl'])) unset($this->data['newurl']);*/
      $app->db->update(DB_prefix.'object',$savedata,'id='.intval($this->data['id']));
      if (isset($this->data['newurl']) && $this->data['newurl']) { // если пришел новый URL, то нужно его сохранить тоже
        $urldata['url']=$parent['url'].'/'.$newurl;
        $urldata['oid']=$this->data['oid'];
        $urldata['site']=1; // TODO: доделать мультисайтовость
        $app->db->update(DB_prefix.'url',$urldata,'url="'.$this->slashes($url).' AND oid="'.intval($this->data['id']));
      }
    }
  }

  /** Расчет смещения от стартового элемента по номеру страницы **/
  function get_pages($total,$page,$perpage,$direction=false) {
    global $app;
    if (!$page) $page=1;
    $result['perpage']=$perpage;
    $result['total']=$total;
    $result['pages']=($total>0) ? ceil($total/$perpage) : 1; // если даже элементов нет, то все же одна страница с сообщением об этом быть должна
    $result['page']=$page; // TODO: доделать обработку all
    $result['direction']=$direction;
    if (($page-1)*$perpage>$total || $page<1) $result=false; // неправильный номер страницы, в разделах будет обрабатываться как ошибка
    else $result['start']=($page-1)*$perpage; // TODO: уточнить насчет обратной сортировки, возможно, там будет другая формула
    if ($result===false) { // если в результате расчета окзаалось, что пытаемся обратиться к несуществующей странице
      $app->status=404;
      $app->mode=1;
      $app->message('Страницы с таким номером не существует!',3);
    }
    return $result;
  }
}

/** Класс-обертка для выводимых данных. Его применение теоретически позволит поменять шаблонизатор в дальнейшем в случае необходимости **/
class Html {
  var $data; // здесь хранятся данные для вывода объекта
  var $subitems; // вложенные шаблоны (объекты класса Html) для рекурсивной обработки
  var $filename; // здесь хранится имя шаблона

  function Html($file,$module=false) {
    $this->data=array();
    if (!$module) $this->filename = $file.'.htm';
    else $this->filename = $module.'/'.$file.'.htm';
  }

  /** Запоминание элемента данных для последующей выдачи в шаблоне. Для Twig допускаются хеши, объекты, строки и числа **/
  function set($key,$value=false) {
    if (!is_string($key)) trigger_error('Некорректный ключ для массива',E_USER_WARNING);
    $this->data[$key]=$value;
  }

  /** Добавление структуры данных для выдачи в шаблоне. Если уже есть запомненные элементы, новые будут добавлены к ним **/
  function set_all($newdata) {
    $this->data=array_merge($this->data,$newdata);
  }

  /** Добавление для вывода вложенного шаблона, который будет обрабатываться рекурсивно **/
  function subtemplate($key,$template) {
    $this->subitems[$key][]=$template;
  }

}

class Parser {
  /** Основная функция парсера -- преобразование объектов с данными (класс Hhtml) в реальный HTML-код
   *
   * @param string $template -- имя каталога с шаблонами
   * @param object $main_data -- данные (объект Html), которые требуется парсить
   * @return string --
   */
  function parse($template,$data) {
    global $app;
    require_once('lib/Twig/Autoloader.php');
    Twig_Autoloader::register();
    $loader = new Twig_Loader_Filesystem('template/'.$template,'template/default');
    $twig = new Twig_Environment($loader, array( 'cache' => 'cache/styles', 'debug' => $GLOBALS['config']['debug'] ));

    /*    if ($status==200 && ($mode==0 || $mode==4)) { // если страница выводится в нормальном режиме
      foreach ($block_data as $key=>$value ) {
        $buffer[$key]=''; // сначала буфер блока пуст
        for ($i=0, $count=count($block_data[$key]); $i<$count; $i++) {
          $curtmpl = $twig->loadTemplate($block_data[$key][$i]->filename);
          $buffer[$key].=$curtmpl->render($block_data[$key][$i]->data);
        }
      }
      if (is_object($main_data)) {
        $action_tpl=$twig->loadTemplate($main_data->filename);
        $buffer['CONTENT']=$action_tpl->render($main_data->data);
      }
      else $buffer['CONTENT']='';

      if ($mode==4) $main_tpl=$twig->loadTemplate('admin.htm');
      else $main_tpl=$twig->loadTemplate('index.htm');

      $html=$main_tpl->render($buffer);
    }
    elseif ($status==404 || $status==403) {
      $main_tpl=$twig->loadTemplate('error.htm');
      $html=$main_tpl->render($buffer);
    }
    elseif (($status==301 || $status==302) && $mode==0) {
      if ($this->user->session()) {
        $html=false;
        if (count($this->messages)>0) $_SESSION['messages']=$this->messages; // буферизуем сообщения в сессию, чтобы вывести их на следующей нормальной странице и избавиться тем самым от вывода промежуточной страницы с редиректом
      }
      else {
        $main_tpl=$twig->loadTemplate('message.htm');
        $html=$main_tpl->render($buffer);
      }
    }
    elseif ($mode==1) { // если режим вывода -- информационное сообщение
      $main_tpl=$twig->loadTemplate('message.htm');
      $html=$main_tpl->render($buffer);
    }
    elseif ($mode==2) { // если режим вывода -- AJAH, рендерим только часть HTML-кода с текущим действием
      $action_tpl=$twig->loadTemplate($main_data->filename);
      $html=$curtmple->render($main_data->data);
    }
    elseif ($mode==3) { // если режим вывода -- почтовое сообщение, рендерим часть кода с текущим действием и общий почтовый шаблон mail.htm
      if (is_object($main_data)) {
        $action_tpl=$twig->loadTemplate($main_data->filename);
        $buffer['CONTENT']=$action_tpl->render($main_data->data);
      }
      $main_tpl=$twig->loadTemplate('mail.htm');
      $html=$main_tpl->render($buffer);
    }*/
    if (is_object($data)) {
      if (is_array($data->subitems)) foreach ($data->subitems as $key=>$value) {
        $buffer = '';
        for ($i=0, $count=count($value); $i<$count; $i++) $buffer.=$this->parse($template, $value[$i]);
        $data->data[$key]=$buffer;
      }
      $action_tpl=$twig->loadTemplate($data->filename);
      if (is_array($data->data)) $html=$action_tpl->render($data->data);
    }
    else $html='';

    return $html;
  }
}

/** Помещение информации в отладочный массив **/
function _dbg() {
  $GLOBALS['config']['debug']=1;
  if ($GLOBALS['config']['debug']!=0) {
    if (!isset($GLOBALS['IntBF_debug'])) $GLOBALS['IntBF_debug']='';
    $GLOBALS['IntBF_debug'].='<p>';
    foreach (func_get_args() as $name=>$value) {
      if (is_array($value) || is_object($value)) $GLOBALS['IntBF_debug'].=$name.': '.nl2br(str_replace('  ','&nbsp;',print_r($value,true)));
      else $GLOBALS['IntBF_debug'].=$name.': '.htmlspecialchars($value).' ';
    }
    $GLOBALS['IntBF_debug'].="</p>\n";
  }
}

global $app;
if (realpath($_SERVER['SCRIPT_FILENAME'])==__FILE__) { // создаем экземпляр приложения и запускаем его только в том случае, если вызов сделан напрямую
  /** @var Application **/
  $app = new Application;
  $app->main();
}
?>  
